/*
 * Copyright 2007 Brian Watkins
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */


package bw.news;

import java.net.*;
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
import com.sun.syndication.feed.synd.*;
import com.sun.syndication.io.*;
import bw.util.*;
import bw.refresh.*;
import bw.news.plugin.NewsSourcePlugin;

public class NewsSource extends RefreshingDataSource implements Serializable
{
    static final long serialVersionUID = 8454458372764794740L;

    public static final String NEWS_SOURCE_KEY = "NEWS_SOURCE-";

    public static final long DEFAULT_REFRESH = 30 * 60 * 1000L;

    private String _name = null;
    private int _primaryId = -1;
    private String _siteURL = null;
    private NewsFeed _feed = new NewsFeed();
    private String _pluginName = null;
    private transient NewsSourcePlugin _plugin = null;

    public static String getKey ( String id )
    {
        return NEWS_SOURCE_KEY + id;
    }

    public NewsSource ( int id, String name, String src, String site, int refresh )
    {
        super( getKey( String.valueOf( id ) ), src, refresh * 60 * 1000L );

        _primaryId = id;
        _name = name;
        _siteURL = site;
    }

    public NewsSource ( int id, String name, String src )
    {
        super( getKey( String.valueOf( id ) ), src, DEFAULT_REFRESH );

        _primaryId = id;
        _name = name;
    }

    

    public void setRefresh ( long refreshMillis )
    {
        super.setSleepTime( refreshMillis );
    }

    public void setRefreshMinutes ( int min )
    {
        super.setSleepTime( min * 60 * 1000L );
    }

    public void setSiteURL ( String url )
    {
        _siteURL = url;
    }

    public void handleUpdateFailure ()
    {
        //add a dummy news item to show the error.
        NewsItem error = makeErrorItem( "Problem getting news feed.", getErrorMessage() );
        _feed.setErrorNews( error );
    }

    private NewsItem makeErrorItem ( String message, String errorStr )
    {
        NewsItem error = new NewsItem();
        StringBuffer buf = new StringBuffer();
        buf.append( message );
        buf.append( "<p class='newsError'>" );
        buf.append( Util.XMLEscape( errorStr ) );
        buf.append( "</p>" );
        error.setDescription( buf.toString() );
        error.setAllowStar( false );

        return error;
    }

    public void setNewNewsItemsSeen () 
    { 
        if ( !_feed.newItemsSeen() )
        {
            _feed.setNewItemsSeen();

            //sync the news source at this time. 
            //unless we are in an error situation.
            //that will make the recent changes persistent. 
            if ( !_feed.hasError() )
            {
                sync();
            }
        }
    }

    public boolean setNewsItemStarred ( String id, boolean b )
    {
        boolean success = _feed.setItemStarred( id, b );
        if ( success )
        {
            sync();
        }
        return success;
    }

    public Iterator getNewsItems ()
    {
        //only try to do this if we aren't doing background refreshes
        if ( !Config.isPropertyTrue( NewsServant.DO_BACKGROUND_NEWS_REFRESH ) )
        {
            if ( shouldRefresh() )
            {
                Log.getInstance().write( "Manually updating news for: " + _name );
                try
                {
                    updateSource();
                }
                catch ( RefresherException ex )
                {
                    Log.getInstance().write( "Problem manually updating news for: " + _name, ex );
                }
            }
        }

        return _feed.iterator();
    }

    public void beginUpdate ()
    {
        //if the new items have been viewed then we reset them.
        //if not then we keep them around as new. This allows us to 
        //only show items that are new or unseen on the interface,
        //so we don't mark them as old until there are new items to put up. 
        //this logic is handled inside this method
        _feed.markNewsItemsOldIfSeen();
    }

    public void refreshComplete ()
    {
        //save state
        sync();
    }

    public void updateSource ( HttpURLConnection conn ) throws RefresherException
    {
        try
        {
            //try it without the xml cleaner for the time being
            //String doc = Util.readStream( new XmlCleaner( conn.getInputStream() ) );

            //assume the doc is in UTF-8. might need to be made smarter
            String doc = Util.readStream( conn.getInputStream(), "UTF-8" );
            
            if ( doc.trim().length() == 0 )
            {
                RefresherException rex = new RefresherException( "Error updating source for " + 
                                                                 toString() + 
                                                                 ": no document found" );
                rex.setRecoverable( false );
                throw rex;
            }

            Log.getInstance().write( "Updating News Source: " + toString() );

            //now try to parse the document
            //perhaps dangerous kind of loop. but this allows to deal with errors.
            //when a parsing error is found, we remove the item in which it is found
            //and try again with the new doc. 
            NewsFeed newFeed = new NewsFeed();

            SyndFeed feed = null;
            while ( feed == null )
            {
                try 
                {
                    SyndFeedInput input = new SyndFeedInput();
                    feed = input.build( new StringReader( doc ) );
                }
                catch ( ParsingFeedException pfex )
                {
                    Log.getInstance().write( "Error in feed " + toString() + " ==> " + 
                                             pfex.getMessage(), pfex );
                    doc = removeItemAtLine( doc, pfex.getLineNumber(), pfex.getColumnNumber() );

                    //add an error message to the feed so we know there was a problem
                    newFeed.add( makeErrorItem( "Problem getting news item.", 
                                                pfex.getMessage() ) );
                }
            }
            
            //get the items from this feed
            newFeed.addAll( parseFeed( feed ) );

            //update my feed with the new list. 
            //this will remove old items, keep any items that are the same,
            //and add new items
            _feed.updateFeed( newFeed );
        }
        catch ( Exception ex )
        {
            RefresherException rex = new RefresherException( "Error updating source for " + 
                                                             toString() + ": " +
                                                             ex.getMessage(), ex );
            rex.setRecoverable( false );
            throw rex;
        }
    }

    private NewsFeed parseFeed ( SyndFeed feed )
    {
        Iterator entries = feed.getEntries().iterator();
        NewsFeed newNews = new NewsFeed();

        //specify a cutoff for news stories
        long cutoff = ( DateFactory.getInstance().getDate().getTime() - 
                        ( 24 * 60 * 60 * 1000L ) );
            
        //create the news item objects
        while ( entries.hasNext() )
        {
            SyndEntry entry = (SyndEntry) entries.next();
            NewsItem item = NewsItem.fromSyndEntry( entry );
            
            NewsSourcePlugin plugin = getPlugin();
            if ( plugin != null )
            {
                plugin.operate( item );
            }

            //don't worry about anything more than 1 days old
            if ( !item.hasPublishedDate() || item.getPublishedDate().getTime() > cutoff )
            {
                newNews.add( item );
            }
        }
            
        return newNews;
    }

    private String removeItemAtLine ( String doc, int lineNum, int colNum ) 
        throws IOException, FeedException
    {
        int curLine = 0;
        
        //this will end up being the index of the error, or at least
        //the index of the beginning of the line the error is on. 
        int index = 0;
        
        BufferedReader bin = new BufferedReader( new StringReader( doc ) );
        String line = null;
        while ( ( line = bin.readLine() ) != null )
        {
            if ( ++curLine == lineNum ) 
            {
                index = index + colNum;
                break;
            }
            else
            {
                //add one for line feed character
                index = index + line.length() + 1;
            }
        }

        String itemDelimStr = "";
        if ( doc.indexOf( "<item>" ) > -1 )
        {
            //for rss feeds
            itemDelimStr = "item";
        }
        else
        {
            //for atom feeds
            itemDelimStr = "entry";
        }

        int startItemTag = doc.lastIndexOf( "<" + itemDelimStr + ">", index );
        int endItemTag = doc.indexOf( "</" + itemDelimStr + ">", index ) + 7;

        String newDoc = null;
        if ( startItemTag > -1 && endItemTag > -1 && startItemTag < endItemTag )
        {
            newDoc = doc.substring( 0, startItemTag ) + doc.substring( endItemTag );
        }
        else
        {
            throw new FeedException( "Bad XML in feed at line: " + lineNum );
        }

        //Log.getInstance().write( newDoc );

        return newDoc;
    }

    private NewsSourcePlugin getPlugin ()
    {
        if ( _pluginName != null && _pluginName.length() > 0 )
        {
            if ( _plugin == null )
            {
                try
                {
                    Class pluginClass = Class.forName( _pluginName );
                    Constructor pluginConstructor = pluginClass.getConstructor( new Class[0] );
                    _plugin = (NewsSourcePlugin) pluginConstructor.newInstance( new Object[0] );
                }
                catch ( Exception ex )
                {
                    Log.getInstance().write( "Problem instantiating plugin " + _pluginName + 
                                             " for news source: " + getName() );
                }
            }
        }

        return _plugin;
    }

    public String getPluginName () { return _pluginName; }
    public void setPluginName ( String s ) { _pluginName = s; }

    public int getPrimaryId () { return _primaryId; }

    public String getName ()
    {
        return _name;
    }

    public void setName ( String name )
    {
        _name = name;
    }

    public String getSiteURL ()
    {
        return ( _siteURL == null ? "#" : _siteURL );
    }

    public String getSite ()
    {
        return _siteURL;
    }

    public int getNewItemsCount ()
    {
        return _feed.unseenItemsSize();
    }

    public String getRefreshStr ()
    {
        return String.valueOf( super.getSleepTime() / 1000 / 60 );
    }
    
    public String toString ()
    {
        StringBuffer buf = new StringBuffer();
        buf.append( _name ).append( " (" ).append( getSrc() ).append( ")" );
        return buf.toString();
    }

    public boolean equals ( Object obj )
    {
        return ( getId().equals( ( (NewsSource) obj ).getId() ) );
    }

    public static void main ( String [] args )
    {
        if ( args.length != 1 && args.length != 3 )
        {

            System.out.println( "NewsSource <rss src> [<username> <password>]" );
            System.exit( 0 );
        }

        String rssSrc = args[0];

        NewsSource source = new NewsSource( 0, "testing", rssSrc );
        source.setRefresh( 0 );

        if ( args.length == 3 )
        {
            System.out.println( "Setting username and password" );
            source.setUsername( args[1] );
            source.setPassword( args[2] );
        }

        System.out.println( "Requesting news from: " + source.getSrc() );

        Iterator news = source.getNewsItems();
        while ( news.hasNext() )
        {
            NewsItem item = (NewsItem) news.next();
            System.out.println( "-------------------" );
            System.out.println( "Title: " + item.getTitle() );
            System.out.println( "Contents: " + item.getDescription() );
            if ( item.hasPublishedDate() )
            {
                System.out.println( "Date: " + 
                                    Format.formatMilitary( item.getPublishedDate() ) );
            }
            System.out.println( "Is New: " + item.isNew() );
        }

        System.out.println( "-------------------" );
    }
}